/* 
 * Enterprise Library Extensions - DI + ORM.
 * Copyright (C) 2006
 *
 * GNU General Public License
 *
 * This program is free software; you can redistribute 
 * it and/or modify it under the terms of the GNU General Public License 
 * as published by the Free Software Foundation; either version 2 of the 
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
 * See the GNU General Public License for more details.
 */
using System;
using System.Data;
using System.Data.Common;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using Microsoft.Practices.EnterpriseLibrary.Data;
using ELE.EnterpriseLibrary.Properties;
using System.Configuration;

namespace ELE.EnterpriseLibrary.Data
{
  /// <summary>
  /// Default implementation of the <see cref="IObjectQueryRunner"/> interface that assumes that the
  /// mapping information is provided via the application's configuration file (App.config or Web.config) or
  /// the path to it is provided in the constructor.
  /// <para>Following is an example of using this class:</para>
  /// <code>
  ///  Microsoft.Patterns.EnterpriseLibrary.Data.Database db = 
  ///          Microsoft.Patterns.EnterpriseLibrary.Data.DatabaseFactory.CreateDatabase();
  /// 
  ///  ELE.EnterpriseLibrary.Data.IObjectQueryRunner runner = 
  ///          new ELE.EnterpriseLibrary.Data.ObjectQueryRunner();
  /// 
  ///  Employee inputObject = new Employee();
  ///  inputObject.Id = 0;
  /// 
  ///  // Run a query by its id and return a DataSet
  ///  System.Data.DataSet ds = runner.ExecuteDatasetQueryByName(db, "GetEmployeeById", inputObject);
  /// 
  ///  // Run a query by its id and return an IList of Employee objects
  ///  System.Collections.IList objects = runner.ExecuteQueryByName(db, "GetEmployeeById", inputObject);
  /// 
  ///  // Run a query by its id and return an IList of Employee objects but do not recurse the bindings
  ///  IList lightObjects = runner.ExecuteQueryByNameLightweight(db, "GetEmployeeById", inputObject);
  /// 
  ///  // Run a query by using the primary key defined in the mapping configuration of the identified object
  ///  Employee employee = (Employee)runner.QueryObjectByPrimaryKey(db, "Employee", inputObject.Id);
  /// 
  ///  // Run a query by using the primary key defined in the mapping configuration of the identified object
  ///  // Do not recurse the bindings.
  ///  Employee lightEmployee = (Employee)runner.QueryObjectByPrimaryKeyLightweight(db, "Employee", inputObject.Id);
  /// 
  ///  // Assume that the BuildEmployee() method simply constructs an Employee object with the proper set of data
  ///  Employee upEmployee = this.BuildEmployee();
  /// 
  ///  // Persist the employee object in the database. This will do an insert if the id of the employee is new
  ///  // to the database or it will do an update if it is already in the database.
  ///  int addedRows = runner.PersistObject(db, "Employee", upEmployee);
  ///  
  ///  // Assume that the AddEquipmentToEmployee method adds an equipment object to the employee object.
  ///  this.AddEquipmentToEmployee(upEmployee, 4, "0334442332");
  ///
  ///  // Persist the employee object. This will update all tables that relate to the employee object and 
  ///  // all mapped objects contained within it, including an insert of the equipment just added
  ///  // above to the proper mapped table for the Equipment object.
  ///  int updatedRows = runner.PersistObject(db, "Employee", upEmployee);
  /// 
  ///  // Delete the employee from the database, including all mapped objects contained within it.
  ///  int deletedRows = runner.RemoveObject(db, "Employee", upEmployee);
  /// </code>
  /// <para>The above code assumes that you have a configuration file that contains the proper mapping data
  /// that conforms to the <see cref="ELE.EnterpriseLibrary.Data.Configuration.ObjectMappingSettings"/> requirements. 
  /// Furthermore, since the default constructor is used, this file is actually the App.config or Web.config of the 
  /// application where this code resides.</para>
  /// <para>Notice the dependency on the <see cref="Microsoft.Practices.EnterpriseLibrary.Data.Database"/> object
  /// which suggest that the proper configuration settings also exist in the application's configuration file
  /// for the EL's Data Access Application Block</para>
  /// </summary>
  public class ObjectQueryRunner : ObjectQueryRunnerBase
  {
    /// <summary>
    /// Default constructor
    /// </summary>
    public ObjectQueryRunner()
    {
    }
    /// <summary>
    /// Initialize a runner not using the default app.config but a specific configuration file.
    /// </summary>
    /// <param name="configurationFilePath"></param>
    public ObjectQueryRunner(string configurationFilePath)
    {
      ObjectSqlFactory.InitializeSource(configurationFilePath);
    }

    #region Mapping helper methods
    /// <summary>
    /// Takes a list of object sql mappings referenced by a query or stored procedure and fills it up with information from the mappings section.
    /// It uses the references to overwrite values as well.
    /// </summary>
    /// <param name="source"></param>
    /// <returns></returns>
    protected List<ObjectSqlMapping> GetOverwrittenMappings(List<ObjectSqlMapping> source )
    {
      if (source != null)
      {
        List<ObjectSqlMapping> overwritten = new List<ObjectSqlMapping>();
        foreach (ObjectSqlMapping map in source)
        {

          ObjectSqlMappingManager mapManager = this.GetMappingManager(map.MapName);
          if (mapManager != null)
          {
            ObjectSqlMapping objectMapping = (ObjectSqlMapping)mapManager.GetMapping(map.Name).Clone();
            objectMapping.MapName = map.MapName;
            this.OverwriteMapping(objectMapping, map);
            overwritten.Add((ObjectSqlMapping)objectMapping.Clone());
            return overwritten;
          }
          else
          {
            throw new ConfigurationErrorsException(string.Format(Resources.Culture, Resources.ExceptionMappingReferenceIsInvalid, map.MapName, map.Name));
          }
        }
      }
      return null;
    }
    /// <summary>
    /// Resolves stored procedure mappings and stored procedure cursor mapping with settings from the mappings section.
    /// </summary>
    /// <param name="objectStoredProcedure"></param>
    /// <returns></returns>
    protected ObjectStoredProcedure ResolveStoredProcedureMappings(ObjectStoredProcedure objectStoredProcedure)
    {
      ObjectStoredProcedure procedure = (ObjectStoredProcedure)objectStoredProcedure.Clone();
      procedure.Mappings = this.GetOverwrittenMappings(objectStoredProcedure.Mappings);
      if (procedure.Cursors != null)
      {
        foreach (ObjectStoredProcedureCursor cursor in procedure.Cursors)
        {
          cursor.Mappings = this.GetOverwrittenMappings(cursor.Mappings);
        }
      }
      return procedure;
    }
    /// <summary>
    /// Resolves the <see cref="ObjectSqlMapping"/> contained within a <see cref="ObjectSqlQuery"/>
    /// and populates the object sql query object properly.
    /// </summary>
    /// <param name="objectQuery">The query object.</param>
    /// <returns>The populated query object.</returns>
    protected ObjectSqlQuery ResolveQueryMappings(ObjectSqlQuery objectQuery)
    {
      ObjectSqlQuery query = (ObjectSqlQuery)objectQuery.Clone();
      query.Mappings = this.GetOverwrittenMappings(objectQuery.Mappings);
      return query;
    }
    #endregion

    #region General helper methods
    /// <summary>
    /// Retrieves a <see cref="ObjectSqlQueryManager"/> from the <see cref="ObjectSqlFactory"/> that
    /// is identified by the given id.
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public virtual ObjectStoredProcedureManager GetStoredProcedureManager(string id)
    {
      if (string.IsNullOrEmpty(id))
      {
        return null;
      }
      else
      {
        return ObjectSqlFactory.GetObjectStoredProcedureManager(id);
      }
    }
    /// <summary>
    /// Retrieves a <see cref="ObjectSqlQueryManager"/> from the <see cref="ObjectSqlFactory"/> that
    /// is identified by the given id.
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public virtual ObjectSqlQueryManager GetQueryManager(string id)
    {
      if (string.IsNullOrEmpty(id))
      {
        return ObjectSqlFactory.GetObjectSqlQueryManager();
      }
      else
      {
        return ObjectSqlFactory.GetObjectSqlQueryManager(id);
      }
    }
    /// <summary>
    /// Retrieves a <see cref="ObjectSqlMappingManager"/> from the <see cref="ObjectSqlFactory"/> that
    /// is identified by the given id.
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public virtual ObjectSqlMappingManager GetMappingManager(string id)
    {
      if (string.IsNullOrEmpty(id))
      {
        return ObjectSqlFactory.GetObjectSqlMappingManager();
      }
      else
      {
        return ObjectSqlFactory.GetObjectSqlMappingManager(id);
      }
    }
    /// <summary>
    /// Retrieves a specific <see cref="ObjectSqlMapping"/> with the given objectId 
    /// from a <see cref="ObjectSqlMappingManager"/> created by
    /// the <see cref="ObjectSqlFactory"/> that is identified by the given mappingId.
    /// </summary>
    /// <param name="mappingId"></param>
    /// <param name="objectId"></param>
    /// <returns></returns>
    public override ObjectSqlMapping GetMapping(string mappingId, string objectId)
    {
      ObjectSqlMappingManager mapManager = GetMappingManager(mappingId);
      return mapManager.GetMapping(objectId);
    }
    #endregion

    #region Select query helper methods

    /// <summary>
    /// Helper method
    /// </summary>
    /// <param name="database"></param>
    /// <param name="queryId"></param>
    /// <param name="inputObject"></param>
    /// <param name="recursive"></param>
    /// <returns></returns>
    protected override IList ExecuteQueryByNameHelper(Database database, string queryId, object inputObject, bool recursive)
    {
      IList results = new ArrayList();
      ObjectSqlQueryManager manager = this.GetQueryManager(queryId);
      ObjectSqlQuery query = this.ResolveQueryMappings(manager.Query);
      IDataReader reader = this.ExecuteDatareaderQueryByNameHelper(database, query, inputObject);
      while (reader.Read())
      {
        foreach (ObjectSqlMapping mapping in query.Mappings)
        {
          results.Add(this.GetObjectFromReaderPosition(database, reader, mapping, recursive));
        }
      }
      return results;
    }
    #endregion

    #region IObjectQueryRunner Members
    /// <summary>
    /// Executes a stored procedure and returns a data set object from the results.
    /// </summary>
    /// <param name="database"></param>
    /// <param name="storedProcedureId"></param>
    /// <param name="inputObject"></param>
    /// <returns></returns>
    public override DataSet ExecuteDatasetStoredProcedureByName(Database database, string storedProcedureId, object inputObject)
    {
      ObjectStoredProcedureManager manager = this.GetStoredProcedureManager(storedProcedureId);
      ObjectStoredProcedure storedProcedure = this.ResolveStoredProcedureMappings(manager.StoredProcedure);
      using (DbCommand command = this.PrepareStoredProcedureCommandInputs(database, storedProcedure, inputObject))
      {
        DataSet resultDataSet = null;

        resultDataSet = database.ExecuteDataSet(command);

        // Note: connection was closed by ExecuteDataSet method call 

        return resultDataSet;
      }
    }
    /// <summary>
    /// Executes a stored procedure and returns a list of objects built based on the associated mappings.
    /// </summary>
    /// <param name="database"></param>
    /// <param name="storedProcedureId"></param>
    /// <param name="inputObject"></param>
    /// <param name="recursive"></param>
    /// <returns></returns>
    protected override IList ExecuteStoredProcedureByNameHelper(Database database, string storedProcedureId, object inputObject, bool recursive)
    {
      throw new NotImplementedException("The method or operation is not implemented.");
    }
    /// <summary>
    /// Implementation of the interface method.
    /// See <see cref="IObjectQueryRunner.ExecuteDatasetQueryByName(Database, string, object)"/> for more information.
    /// </summary>
    /// <param name="database"></param>
    /// <param name="queryId"></param>
    /// <param name="inputObject"></param>
    /// <returns></returns>
    public override DataSet ExecuteDatasetQueryByName(Database database, string queryId, object inputObject)
    {
      ObjectSqlQueryManager manager = this.GetQueryManager(queryId);
      ObjectSqlQuery query = this.ResolveQueryMappings(manager.Query);
      using (DbCommand command = this.PrepareCommandInputs(database, query, inputObject))
      {
        DataSet resultDataSet = null;

        resultDataSet = database.ExecuteDataSet(command);

        // Note: connection was closed by ExecuteDataSet method call 

        return resultDataSet;
      }
    }
    /// <summary>
    /// Implementation of the interface method.
    /// See <see cref="IObjectQueryRunner.ExecuteDatareaderQueryByName(Database, string, object)"/> for more information.
    /// </summary>
    /// <param name="database"></param>
    /// <param name="queryId"></param>
    /// <param name="inputObject"></param>
    /// <returns></returns>
    public override IDataReader ExecuteDatareaderQueryByName(Database database, string queryId, object inputObject)
    {
      ObjectSqlQueryManager manager = this.GetQueryManager(queryId);
      ObjectSqlQuery query = this.ResolveQueryMappings(manager.Query);
      return this.ExecuteDatareaderQueryByNameHelper(database, query, inputObject);
    }
    #endregion
  }
}
